package com.noassertions.web;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.iscreen.ValidationException;
import org.iscreen.ValidationServiceWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.noassertions.data.DAOInterface;
import com.noassertions.domain.Fit;
import com.noassertions.domain.Image;
import com.noassertions.domain.Rider;
import com.noassertions.domain.User;
import com.noassertions.services.KdTreeService;
import com.noassertions.services.LuceneIndexer;
import com.noassertions.utils.FileUtil;
import com.noassertions.utils.ImageUtil;

@Controller
public class FitController {

	protected final Log log = LogFactory.getLog(getClass());
	private DAOInterface dao;
	private ValidationServiceWrapper fitValidator;
	private ValidationServiceWrapper riderValidator;
	private MessageSource messageSource;
    private final LuceneIndexer luceneIndexer;
    private final KdTreeService kDTreeService;

    @Autowired
    public FitController(DAOInterface dao, ValidationServiceWrapper fitValidator, ValidationServiceWrapper riderValidator, MessageSource messageSource, LuceneIndexer luceneIndexer, KdTreeService kDTreeService) {
        this.dao = dao;
        this.fitValidator = fitValidator;
        this.riderValidator = riderValidator;
        this.messageSource = messageSource;
        this.luceneIndexer = luceneIndexer;
        this.kDTreeService = kDTreeService;
    }  
	
    // get all fits for a user and return as a json object
	@RequestMapping(value = "/fit/{userid}", method = RequestMethod.GET)
	public String getFit(@PathVariable Integer userid, Model model)
	{
		ArrayList fits = dao.getFitsByUserId(userid);
		model.addAttribute("fits", fits);
		return "fit";
	}
		
	/**
	 * Display form for new fit
	 */
	@RequestMapping(value = "/fit/new", method = RequestMethod.GET)
	public String newRiderDisplay(Model model)
	{
		
		//TODO: if user has already entered rider data, populate the form with that data
		UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
		model.addAttribute("emailAddress", principal.getUsername());
		
		// User submitted info is complete, so let's validate
		boolean valid = true;
		User user = dao.getUserByemailAddress(principal.getUsername());
		
		// Verify that user has complete user and rider info before allowing
		// them to contribute fits
		// TODO we shouldn't have to get a list and then get the 0 element
		// of that list. find a better way.
		ArrayList riderList = dao.getRiderByUserId(user.getUserid());
		if (riderList.size() == 0) {
			valid = false;
		} else {

			Rider rider = (Rider) riderList.get(0);
			try {
				riderValidator.validate(rider);
			} catch (ValidationException e) {
				log.debug("Rider is not complete (did not validate) not allowing user to contribute new fit");
				valid = false;
			}
		}

		if (!valid) {
			model.addAttribute("noRiderDataError", true);
		}
		
		return "newfit";
	}
		

    /**
     * This method receives a new fit object from a spring form and will insert into DB if it is valid
     * This is the standard POST for the Fit endpoint in a RESTful SOA 
     * 
     * Since Spring does not support multiple files in the multipart post, we'll parse post ourselves 
     * (unlike other controllers where we use Spring's handy command/bind functionality)
     * @throws Exception 
     */
	@RequestMapping(value = "/fit", method = RequestMethod.POST)
	public String createNewFit(HttpServletRequest request, Model model) throws Exception
	{
		// if model.hasattribute("fit"), we've received the request from this.newFit()
		
		boolean isMultipart = ServletFileUpload.isMultipartContent(request);

		if (isMultipart) {

			Fit fit = new Fit();
			Set<Image> images = new HashSet<Image>();
			ServletFileUpload upload = new ServletFileUpload();
			FileItemIterator iter = upload.getItemIterator(request);
			int numImages = 0;
			boolean imageTooBig = false;
			while (iter.hasNext()) {
				FileItemStream item = iter.next();
				String name = item.getFieldName();
				InputStream inputStream = item.openStream();
				// Get fields from form and add them to the fit bean
				if (item.isFormField()) {
					// TODO: this form parsing should be factored out. values should also be validated
					if (name.equals("dateofmeasurement")) {
						String dateString = Streams.asString(inputStream);
						SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
						Date date = format.parse(dateString);
						fit.setDateofmeasurement(date);
						model.addAttribute("saddleheight", dateString);
					}
					if (name.equals("saddleheight")) {fit.setSaddleheight(Float.valueOf(Streams.asString(inputStream)));model.addAttribute("saddleheight", fit.getSaddleheight());}
					if (name.equals("barswidth")) {fit.setBarswidth(Float.valueOf(Streams.asString(inputStream)));model.addAttribute("barswidth", fit.getBarswidth());}
					if (name.equals("floortobars")) {fit.setFloortobars(Float.valueOf(Streams.asString(inputStream)));model.addAttribute("floortobars", fit.getFloortobars());}
					if (name.equals("floortosaddle")) {fit.setFloortosaddle(Float.valueOf(Streams.asString(inputStream)));model.addAttribute("floortosaddle", fit.getFloortosaddle());}
					if (name.equals("centertobars")) {fit.setCentertobars(Float.valueOf(Streams.asString(inputStream)));model.addAttribute("centertobars", fit.getCentertobars());}
					if (name.equals("centertosaddle")) {fit.setCentertosaddle(Float.valueOf(Streams.asString(inputStream)));model.addAttribute("centertosaddle", fit.getCentertosaddle());}
					if (name.equals("notes")) {fit.setNotes(Streams.asString(inputStream));model.addAttribute("notes", fit.getNotes());}
					if (name.equals("frame")) {fit.setFrame(Streams.asString(inputStream));model.addAttribute("frame", fit.getFrame());}
					if (name.equals("shifters")) {fit.setShifters(Streams.asString(inputStream));model.addAttribute("shifters", fit.getShifters());}
					if (name.equals("saddle")) {fit.setSaddle(Streams.asString(inputStream));model.addAttribute("saddle", fit.getSaddle());}
					if (name.equals("pedals")) {fit.setPedals(Streams.asString(inputStream));model.addAttribute("pedals", fit.getPedals());}
					if (name.equals("stem")) {fit.setStem(Streams.asString(inputStream));model.addAttribute("stem", fit.getStem());}
					if (name.equals("bars")) {fit.setBars(Streams.asString(inputStream));model.addAttribute("bars", fit.getBars());}
					if (name.equals("post")) {fit.setPost(Streams.asString(inputStream));model.addAttribute("post", fit.getPost());}
					if (name.equals("cranks")) {fit.setCranks(Streams.asString(inputStream));model.addAttribute("cranks", fit.getCranks());}
				}
				// If the field is a file field, we should have an attached image
				// write that image to disk and insert its info in the DB
				else {
					log.debug("File field " + name + " with file name " + item.getName() + " detected.");
					if (!"".equals(item.getName()) && numImages < 5) {
						// Clean filename string
						String cleanedFileName = item.getName().replaceAll("[^\\w.-]*", "");						
						// Use FileUtil.findStorageLocation to make sure our filename hasn't been used before
						File destination = FileUtil.findStorageLocaiton(cleanedFileName, messageSource.getMessage("imagestorepath", null, Locale.getDefault()));
						log.debug("Cleaned filename string: " + cleanedFileName);
						
						//Write original image to disk
						OutputStream outputStream = new FileOutputStream(destination);
						byte[] buffer = new byte[1024];
						int length;
						int totalSize = 0;		

						// Write file to disk until we reach the end of the input stream or until the
						// input stream becomes bigger than ~1MB
						while ((length = inputStream.read(buffer)) > 0 && totalSize < 1248576) {
							outputStream.write(buffer, 0, length);
							totalSize = totalSize + 1024;
						}
						outputStream.close();
						
						// If we've exceeded 1MB and there's something left in length, file is tool large
						// delete the partially written file and return error to user
						// TODO: actually alert user that > 1MB file was not stored
						if (length > 0) {
							log.debug("Submitted image was too large.");
							destination.delete();
							imageTooBig = true;
						} else {  // Pass go and collect your $200
							log.debug("Created new image on disk: " + destination.getAbsolutePath());
							numImages++;
							//Create smaller image that is a max of 610 wide and write to disk
							InputStream originalImage = new FileInputStream(destination);
							InputStream resizedImageInStream = ImageUtil.scaleImage(originalImage, 620, 800);
							String resizedImagePath = destination.getParentFile() + "/s_" + destination.getName();
							File resizedImageFile = new File(resizedImagePath);
					        OutputStream resizedImageOutStream = new FileOutputStream(resizedImageFile);
					        byte[] resizedByteBuffer = new byte[1024];
					        int len;
					        while ((len = resizedImageInStream.read(resizedByteBuffer)) > 0) {
					        	resizedImageOutStream.write(resizedByteBuffer, 0, len);
					        }
					        resizedImageInStream.close();
					        resizedImageOutStream.close();
					        log.debug("Created resized image on disk: " + resizedImagePath);
					        
					        // Add image info to the bean
							Image image = new Image();
							image.setFilename(destination.getName());
							image.setLabel(destination.getName());
							images.add(image);
						}
					}
				}
			}
			
			UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
			model.addAttribute("emailAddress", principal.getUsername());
			User user = dao.getUserByemailAddress(principal.getUsername());
			fit.setUserid(user.getUserid());

			// User submitted info is complete, so let's validate
			boolean fitValid = true;
			StringBuffer errors = new StringBuffer();
			
			// This business of catching validation issues is not good, but it's the iscreen approach
			try {
				// TODO: right now, we only see that the date field is not null (since we check format above), we should have a better date validator
				// TODO: if the user submits a non-double value, what happens when we try to cast it as a double above?
				fitValidator.validate(fit);
			} catch (ValidationException e) {
				fitValid = false;
				ArrayList<String> failures = (ArrayList<String>) e.getFailureMessages();
				
				for (String failure : failures) {
					errors.append(failure); 
				}
			}
			
			if (fitValid && !imageTooBig) {
				log.debug("It appears as is if the submitted fit is valid.");
				dao.saveFit(fit);
				luceneIndexer.indexFit(fit);
				kDTreeService.addNewFit(fit);
				
				// TODO: validate images?
				for (Image image : images) {
					image.setFitid(fit.getFitid());
					dao.saveImage(image);
				}
				
				// User's profile is complete at least one fit is associated with profile
				// TODO: this needs some cleanup. as it is, a user may have uploaded all images that were over 1MB and thus it appears
				// as if we have images, but none are written to disk. do a validation on the whole fit bean, or validate all images
				user.setCompleteprofile(true);
				dao.insertUser(user);

				return "redirect:" + messageSource.getMessage("webAppUrl", null, Locale.getDefault()) + "fit/new/confirm";
			}
			else {
				// TODO: We should append this message as part of the validation framework instead of hacking it in this way.
				errors.append(" Please ensure that all images are less than 1MB in size and are of type jpg.");
				model.addAttribute("validationErrors", errors.toString());
				log.debug("fit submission did not validate");		
				// Return orig form with our errors in the model
				return "newfit";
			}
			
			
		}
		else {
			log.debug("Expecting multipart post, but did not receive multipart form");
		}
		
		// Is a user current logged in? Check for null since we setup spring security to permitAll requests to this URL
		// TODO this business of checking for a string (which indicates that user is not authenticated) is a total hack and should be factored out
		if (! (SecurityContextHolder.getContext().getAuthentication().getPrincipal() instanceof String)) {
			UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
			model.addAttribute("emailAddress", principal.getUsername());
		}		
		
		return "newfitconfirm";
	}

	/**
	 * Display form for new user sign up
	 */
	@RequestMapping(value = "/fit/new/confirm", method = RequestMethod.GET)
	public String newFitDisplay(Model model)
	{
		UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
		model.addAttribute("emailAddress", principal.getUsername());

		return "newfitconfirm";
	}
	
	// accept fitid and return userid that owns fitid
	@RequestMapping(value = "/fit/userfromfit/{fitid}", method = RequestMethod.GET)
	public String getUserIdFromFitId(@PathVariable Integer fitid, Model model)
	{
		User user = dao.getUserByFitId(fitid);
		model.addAttribute("user", user);
		return "userfromfit";
	}
}